﻿using System;
using System.Collections.Generic;
using System.Reflection;
using System.Configuration;

namespace BizElements.BusinessLayer
{
    #region Documentation.
    /// <summary>
    /// Configurable utility class that simplifies and automates common operations with <see cref="IAppSetting"/> and <see cref="IAppSettingsManager"/> objects.
    /// </summary>
    /// <remarks><para>The class must be setup at application startup or configured in a *.config file. Example:
    /// <code>&lt;add key="BizElements.BusinessLayer.SettingsUtil.CurrentApplication" value="MySolution"/&gt;</code></para>
    /// <para>By default, SettingsUtil will use <b>BizElements.Web.CommonGetters.GetActor</b> method as current actor provider. 
    /// This can be overrided in <b>appSettings</b> section of Web.config file. Example:
    /// <code>
    /// &lt;add key="BizElements.BusinessLayer.SettingsUtil.ActorProviderAssembly" value="BizElements.Web"/&gt;
    /// &lt;add key="BizElements.BusinessLayer.SettingsUtil.ActorProviderClass" value="BizElements.Web.CommonGetters"/&gt;
    /// &lt;add key="BizElements.BusinessLayer.SettingsUtil.ActorProviderMethod" value="GetActor"/&gt;</code></para>
    /// <para>By default, SettingsUtil uses <see cref="BizElements.BusinessLayer.AppSettingsManager"/> to manage settings in database.
    /// This can be overrided in <b>appSettings</b> section of Web.config file. Example:
    /// <code>
    /// &lt;add key="BizElements.BusinessLayer.SettingsUtil.ManagerAssembly" value="BizElements.BusinessLayer"/&gt;
    /// &lt;add key="BizElements.BusinessLayer.SettingsUtil.ManagerClass" value="BizElements.BusinessLayer.AppSettingsManager"/&gt;</code></para>
    /// </remarks>
    #endregion
    public static class SettingsUtil
    {
        #region Configuration.

        /// <summary>The object which manages settings in the data storage.</summary>
        /// <value>IAppSettingsManager implementation. Default is <see cref="AppSettingsManager"/>.</value>
        /// <remarks>
        /// <para>By default, SettingsUtil uses <see cref="BizElements.BusinessLayer.AppSettingsManager"/> to manage setting in database.
        /// This can be override in <b>appSettings</b> section of Web.config file. Example:
        /// <code>
        /// &lt;add key="BizElements.BusinessLayer.SettingsUtil.ManagerAssembly" value="BizElements.BusinessLayer"/&gt;
        /// &lt;add key="BizElements.BusinessLayer.SettingsUtil.ManagerClass" value="BizElements.BusinessLayer.AppSettingsManager"/&gt;</code></para>
        /// </remarks>
        public static readonly IAppSettingsManager Manager;

        /// <summary>Identifier for the current application.</summary>
        /// <value>Value configured in <b>BizElements.BusinessLayer.SettingsUtil.CurrentApplication</b> app setting. <see cref="String.Empty"/> and <b>null</b> do not represent a valid value.</value>
        public static readonly string CurrentApplication;

        /// <summary>Static parameterless method which returns the current <see cref="IActor"/>.</summary>
        /// <value>Delegate to static method configured in <b>appSettings</b> section of Web.config file. Default is <b>BizElements.Web.CommonGetters.GetActor</b>.</value>
        /// <remarks><para>By default, SettingsUtil will use <b>BizElements.Web.CommonGetters.GetActor</b> method as current actor provider. 
        /// This can be override in <b>appSettings</b> section of Web.config file. Example:
        /// <code>
        /// &lt;add key="BizElements.BusinessLayer.SettingsUtil.ActorProviderAssembly" value="BizElements.Web"/&gt;
        /// &lt;add key="BizElements.BusinessLayer.SettingsUtil.ActorProviderClass" value="BizElements.Web.CommonGetters"/&gt;
        /// &lt;add key="BizElements.BusinessLayer.SettingsUtil.ActorProviderMethod" value="GetActor"/&gt;</code></para></remarks>
        public static Delegates.Function<IActor> ActorProvider;

        static SettingsUtil()
        {
            CurrentApplication = ConfigurationManager.AppSettings["BizElements.BusinessLayer.SettingsUtil.CurrentApplication"];
            if (string.IsNullOrEmpty(CurrentApplication))
                throw new InvalidOperationException("SettingsUtil.CurrentApplication property must be configured in 'BizElements.BusinessLayer.SettingsUtil.CurrentApplication' app setting. See help file for more details.");

            Manager = ReflectionUtil.TryCreateInstanceFromAppSettings("BizElements.BusinessLayer.SettingsUtil.ManagerAssembly", "BizElements.BusinessLayer.SettingsUtil.ManagerClass") as IAppSettingsManager;
            if (Manager == null)
                Manager = new AppSettingsManager();

            string actorGetterAsm = ConfigurationManager.AppSettings["BizElements.BusinessLayer.SettingsUtil.ActorProviderAssembly"] ?? "BizElements.Web";
            string actorGetterClass = ConfigurationManager.AppSettings["BizElements.BusinessLayer.SettingsUtil.ActorProviderClass"] ?? "BizElements.Web.CommonGetters";
            string actorGetterMethod = ConfigurationManager.AppSettings["BizElements.BusinessLayer.SettingsUtil.ActorProviderMethod"] ?? "GetActor";
            ActorProvider = ReflectionUtil.CreateDelegate<Delegates.Function<IActor>>(actorGetterAsm, actorGetterClass, actorGetterMethod);
            if (!ActorProvider.Method.IsStatic)
                throw new InvalidOperationException(string.Format("The method to which SettingsUtil.ActorProvider points must be static. '{0}' is not static.", actorGetterMethod));
        }

        #endregion

        #region Setup module, component, get settings.

        private static string GetModuleName(object module)
        {
            string name;
            if (module is string)
                name = (string)module;
            else if (module is Type)
                name = ((Type)module).ToString();
            else
                name = module.GetType().ToString();

            return name;
        }

        /// <summary>
        /// Gets all settings appliable to the specified module.
        /// </summary>
        /// <param name="module">An object that contains the components to which the settings are applied to. <b>null</b> is not allowed.</param>
        /// <param name="context">Optional <see cref="IAppSetting.Context"/>.</param>
        /// <remarks>The method assumes that the module identifier/name is equal to the fully qualified class name of the provided object.</remarks>
        public static IList<IAppSetting> GetModuleSettings(object module, string context)
        {
            if (module == null)
                throw new ArgumentNullException("module", "Module is required.");

            EnsureMembers(true, true, true);
            IAppSetting filter = CreateFilter(ActorProvider().Id, CurrentApplication, GetModuleName(module), context, null);
            IList<IAppSetting> moduleSettings = Manager.Find(filter);
            return moduleSettings;
        }

        /// <summary>
        /// Loads all settings appliable to the specified module and automatically applies them to all components.
        /// </summary>
        /// <param name="module">An object that contains the components to which the settings are applied to. <b>null</b> is not allowed.</param>
        /// <param name="context">Optional <see cref="IAppSetting.Context"/>.</param>
        /// <remarks>The method assumes that the module identifier/name is equal to the fully qualified class name of the provided object.</remarks>
        public static void SetupModule(object module, string context)
        {
            IList<IAppSetting> moduleSettings = GetModuleSettings(module, context);
            ApplySettingsToModule(module, moduleSettings);
        }

        /// <summary>
        /// Gets all settings appliable to the specified component.
        /// </summary>
        /// <param name="module">An object that contains the component to which the settings are applied to. <b>null</b> is not allowed.</param>
        /// <param name="context">Optional <see cref="IAppSetting.Context"/>.</param>
        /// <param name="component">Identifier/name of the component to which the settings are applied to. <b>null</b> or <see cref="String.Empty"/> are not allowed.</param>
        /// <remarks>The method assumes that the module identifier/name is equal to the fully qualified class name of the provided object.</remarks>
        public static IList<IAppSetting> GetComponentSettings(object module, string context, string component)
        {
            if (module == null)
                throw new ArgumentNullException("module", "Module is required.");

            if (string.IsNullOrEmpty(component))
                throw new ArgumentException("Component name is required.");

            EnsureMembers(true, true, true);
            IAppSetting filter = CreateFilter(ActorProvider().Id, CurrentApplication, GetModuleName(module), context, component);
            IList<IAppSetting> moduleSettings = Manager.Find(filter);
            return moduleSettings;
        }

        /// <summary>
        /// Loads all settings appliable to the specified component and automatically applies them.
        /// </summary>
        /// <param name="module">An object that contains the component to which the settings are applied to. <b>null</b> is not allowed.</param>
        /// <param name="context">Optional <see cref="IAppSetting.Context"/>.</param>
        /// <param name="component">Identifier/name of the component to which the settings are applied to. <b>null</b> or <see cref="String.Empty"/> are not allowed.</param>
        /// <remarks>The method assumes that the module identifier/name is equal to the fully qualified class name of the provided object.</remarks>
        public static void SetupComponent(object module, string context, string component)
        {
            IList<IAppSetting> moduleSettings = GetComponentSettings(module, context, component);
            ApplySettingsToModule(module, moduleSettings);
        }

        /// <summary>
        /// Applies the provided settings to the specified module.
        /// </summary>
        /// <param name="module">An object that contains the components to which the settings are applied to. <b>null</b> is not allowed.</param>
        /// <param name="moduleSettings">Settings which contain values for module components.</param>
        public static void ApplySettingsToModule(object module, IList<IAppSetting> moduleSettings)
        {
            foreach (IAppSetting setting in moduleSettings)
            {
                FieldInfo field;
                PropertyInfo prop;
                bool moduleContainsComponent = ReflectionUtil.FindMember(module, setting.Component, out field, out prop);
                if (moduleContainsComponent)
                {
                    if (setting.PropertyPath.Length == 0)
                    {
                        AssignValueDirectlyToComponent(module, field, prop, setting.Value);
                    }
                    else
                    {
                        object component = (field != null) ? field.GetValue(module) : prop.GetValue(module, null);
                        ReflectionUtil.TrySetValueAtPath(component, setting.PropertyPath, setting.Value, true);
                    }
                }
            }
        }

        #endregion

        #region GetModuleContexts.

        const AppSettingsFilterOptions allSettings = AppSettingsFilterOptions.IncludeContextFreeSettings
            | AppSettingsFilterOptions.IncludeContextualSettings | AppSettingsFilterOptions.IncludeGlobalSettings;

        /// <summary>
        /// Retrieves a list of contexts (setting groups) defined for the provided module.
        /// </summary>
        /// <param name="module">An object that contains the components to which the settings are applied to. <b>null</b> is not allowed.</param>
        /// <returns>List of contexts. Default context has a <b>null</b> value.</returns>
        public static IList<string> GetModuleContexts(object module)
        {
            if (module == null)
                throw new ArgumentNullException("module", "Module is required.");

            IAppSetting filter = CreateFilter(ActorProvider().Id, CurrentApplication, GetModuleName(module), null, null);            
            IList<IAppSetting> moduleSettings = Manager.Find(filter, allSettings);

            List<string> contexts = new List<string>();
            foreach (IAppSetting setting in moduleSettings)
            {
                if (!contexts.Contains(setting.Context))
                    contexts.Add(setting.Context);
            }

            return contexts;
        }

        #endregion

        #region Save.

        /// <summary>
        /// Save new or updates existing user setting.
        /// </summary>
        /// <param name="module">An object that contains the components to which the settings are applied to. <b>null</b> is not allowed.</param>
        /// <param name="context">Optional <see cref="IAppSetting.Context"/>.</param>
        /// <param name="component">The name of the component, typically a controll or a class member, contained in the <see cref="IAppSetting.Module"/>. <b>null</b> is not allowed.</param>
        /// <param name="propertyPathString">Optional path which defines the property of the component which is to receive <see cref="IAppSetting.Value"/>.</param>
        /// <param name="value">The value of the property defined by the <see cref="IAppSetting.PropertyPathString"/>.</param>
        /// <returns>New or updated setting.</returns>
        public static IAppSetting SaveUserSetting(object module, string context, string component, string propertyPathString, object value)
        {
            if (module == null)
                throw new ArgumentException("Module is required.");

            if (string.IsNullOrEmpty(component))
                throw new ArgumentException("Component is required.");

            IAppSetting newSetting = Manager.NewSetting();
            newSetting.ActorId = ActorProvider().Id;
            newSetting.Application = CurrentApplication;
            newSetting.Module = GetModuleName(module);
            newSetting.Context = context;
            newSetting.Component = component;
            newSetting.PropertyPathString = propertyPathString;
            newSetting.Value = value;

            Manager.Save(newSetting);
            return newSetting;
        }

        /// <summary>
        /// Save new or updates existing global setting.
        /// </summary>
        /// <param name="module">An object that contains the components to which the settings are applied to. <b>null</b> is not allowed.</param>
        /// <param name="context">Optional <see cref="IAppSetting.Context"/>.</param>
        /// <param name="component">The name of the component, typically a controll or a class member, contained in the <see cref="IAppSetting.Module"/>. <b>null</b> is not allowed.</param>
        /// <param name="propertyPathString">Optional path which defines the property of the component which is to receive <see cref="IAppSetting.Value"/>.</param>
        /// <param name="value">The value of the property defined by the <see cref="IAppSetting.PropertyPathString"/>.</param>
        public static void SaveGlobalSetting(object module, string context, string component, string propertyPathString, object value)
        {
            if (module == null)
                throw new ArgumentException("Module is required.");

            if (string.IsNullOrEmpty(component))
                throw new ArgumentException("Component is required.");

            IAppSetting newSetting = Manager.NewSetting();
            newSetting.ActorId = null;
            newSetting.Application = CurrentApplication;
            newSetting.Module = GetModuleName(module);
            newSetting.Context = context;
            newSetting.Component = component;
            newSetting.PropertyPathString = propertyPathString;
            newSetting.Value = value;

            Manager.Save(newSetting);
        }

        #endregion

        #region Private helper methods.

        private static IAppSetting CreateFilter(object actorId, string application, string module, string context, string component)
        {
            IAppSetting filter = Manager.NewSetting();
            if (actorId != null)
                filter.ActorId = actorId;

            if (!string.IsNullOrEmpty(application))
                filter.Application = application;

            if (!string.IsNullOrEmpty(module))
                filter.Module = module;

            if (!string.IsNullOrEmpty(context))
                filter.Context = context;

            if (!string.IsNullOrEmpty(component))
                filter.Component = component;

            return filter;
        }

        private static void EnsureMembers(bool ensureManager, bool ensureCurrentApp, bool ensureActorProvider)
        {
            if (ensureManager && Manager == null)
                throw new InvalidOperationException("The Manager property is not yet initialized.");

            if (ensureCurrentApp && CurrentApplication == null)
                throw new InvalidOperationException("The CurrentApplication property is not yet initialized.");

            if (ensureActorProvider && ActorProvider == null)
                throw new InvalidOperationException("The ActorProvider method is not yet assigned.");
        }        

        private static void AssignValueDirectlyToComponent(object module, FieldInfo field, PropertyInfo prop, object val)
        {
            if (field != null)
            {
                try { field.SetValue(module, Convert.ChangeType(val, field.FieldType)); }
                catch { }
            }
            else if (prop != null)
            {
                try { prop.SetValue(module, Convert.ChangeType(val, prop.PropertyType), null); }
                catch { }
            }
        }        

        #endregion
    }    
}
